﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using System.Text.RegularExpressions;
using System.DirectoryServices;

namespace DirectoryProvider
{
	public enum DirectoryType:int
	{
		Undefined = 0,
		FedoraDirectory = 1,
		ActiveDirectory = 2
	}
	
	public class Directory
	{
		
		#region Propiedades
		public DirectoryType Type { get; internal set; }
		
		public string DistinguishedName { get; internal set; }

		public string Server { get; internal set; }

		public int Port { get; internal set; }

		public bool SSL { get; internal set; }
		
		public bool TwoPassQueryGroups { get; internal set; }
		
		internal AttributeMap Map{ get; set; }
		
		public string[] UserAttributes { get; internal set; }

		public string[] GroupAttributes { get; internal set; }
		
		public string[] NamingContexts { get; internal set; }
		
		internal string _defaultNamingContext = null;
		
		private string[] UserMinimalAttributes { get; set; }
		
		public string[] UserObjectClassValues { get; internal set; }
		
		protected string BindUser = null;
		private string BindPass = null;
		
		public string defaultNamingContext { 
			[System.Diagnostics.DebuggerStepThroughAttribute]
			get { 
				if (_defaultNamingContext != null)
					return _defaultNamingContext; 
				if (NamingContexts != null && NamingContexts.Length > 0)
					return NamingContexts [0];
				return "";
			}
		}
		
		public bool Ok { get { return entry != null; } }
		
		internal System.DirectoryServices.DirectoryEntry entry = null;
		#endregion
		
		internal Regex rxLdap = new Regex (@"^(?<proto>\w+):[\/]{1,2}(?<domain>[^:\/\s]+)(:(?<port>[^\/]*))?(|((\/\w+)*\/)(|(?<location>[\w\-\.]+[^#?\s]+)(\?(?<querystring>[^#]*))?(#(?<anchor>.*))?))$", RegexOptions.IgnoreCase);
		//Regex rxLdap = new Regex(@"^(?<proto>\w+):[\/]{1,2}(?<domain>[^:\/\s]+)(:(?<port>[^\/]*))?");


		
		[System.Diagnostics.DebuggerStepThroughAttribute]
		public Directory (string connection, string user, string pass)
		{
			
			NamingContexts = new string[0];
			
			#region Obtener datos desde la cadena de conexión
			
			var m = rxLdap.Match (connection);
			
			//m.Print(rxLdap);
			
			string proto = m.Groups ["proto"].Value.ToUpper ();
			
			switch (proto) {
			case "LDAP":
				Port = 389;
				SSL = false;
				break;
				
			case "LDAPS":
				Port = 689;
				SSL = true;
				break;
			}
			

			{
				int tmpport = (int)m.Groups ["port"].Value.Numeric ();
				if (tmpport > 0)
					Port = tmpport;
			}
			
			Server = m.Groups ["domain"].Value;
			DistinguishedName = m.Groups ["location"].Value;
			
			#endregion
			
			entry = new System.DirectoryServices.DirectoryEntry ();
			entry.AuthenticationType = System.DirectoryServices.AuthenticationTypes.Secure;
			entry.Path = String.Format ("{0}://{1}:{2}", SSL ? "LDAPS" : "LDAP", Server, Port);
			entry.Username = BindUser = user;
			entry.Password = BindPass = pass;
			
			//meter un timeout??
			try {
				foreach (System.DirectoryServices.DirectoryEntry e in entry.Children) {
					//Console.WriteLine (e.Path);
				}
			} catch (Exception ex) {
				entry = null;
				Console.WriteLine (ex.Message);
				return;
			}
			
			
			DirectorySearcher ds = new DirectorySearcher (entry);
			ds.Filter = "(objectClass=*)";
			ds.SearchScope = SearchScope.Base;
			
			var src = ds.FindOne ();
			var vname = "";
			
			foreach (var kvp in src.Parse()) {
				
				Console.WriteLine ("Key: {0} Value: {1}", kvp.Key, kvp.Value);

				switch (kvp.Key.ToLower ()) {
				
				case "defaultnamingcontext": 
					_defaultNamingContext = kvp.Value;
					break;
				
				case "namingcontexts": 
					NamingContexts = kvp.Value.Split (new char[]{';'});
					break;
				
				case "vendorname":
					vname = kvp.Value;
					break;
				}
			
			}

			
			switch (vname) {
			case "Fedora Project":
			case "389 Project":
				Type = DirectoryType.FedoraDirectory;
				break;
			}

			
			
			Map = new AttributeMap ();
			
			UserMinimalAttributes = new string[0];
			
			switch (Type) {
			case DirectoryType.FedoraDirectory:
				PrepareFedora ();
				break;
				
			default:
				throw new Exception ("Directorio no soportado");
			}
			
			
			
			
            
            
            
            
           
            
		}
		
		[System.Diagnostics.DebuggerStepThroughAttribute]
		public bool UserObjectValid (DirectoryEntry entry)
		{
		
			if (UserMinimalAttributes == null || UserMinimalAttributes.Length == 0)
				return true;
			
			
			string[] lst = new string[entry.Properties.PropertyNames.Count];
			int i=0;
			foreach (string prop in entry.Properties.PropertyNames) lst[i++]=prop.ToLower();
			
			var r = UserMinimalAttributes.Except(lst).ToArray();
			
			return r.Length==0;
			
		}
		
		
		[System.Diagnostics.DebuggerStepThroughAttribute]
		public DirectoryEntry NewEntry (string path)
		{
			var entries = entry.Children;
			return entries.Add (path + "," + defaultNamingContext, "Class");
		}
		
		
		[System.Diagnostics.DebuggerStepThroughAttribute]
		public Exception NewEntry (string path, Action<DirectoryEntry> action)
		{
			if (action == null)
				return new Exception ("Accion nula");
			try {
				DirectoryEntry newEntry = entry.Children.Add (path + "," + defaultNamingContext, "Class");
				action (newEntry);
				return null;
			} catch (Exception ex) {
				return ex;
			}
		}
		
		[System.Diagnostics.DebuggerStepThroughAttribute]
		private IEnumerable<SearchResult> Search (bool onlyone, SearchScope scope, string Path, string filter, IEnumerable<string> Attributes)
		{
			
			DirectoryEntry entry = new DirectoryEntry ();
			entry.AuthenticationType = System.DirectoryServices.AuthenticationTypes.Secure;
			entry.Path = String.Format ("{0}://{1}:{2}/{3}", SSL ? "LDAPS" : "LDAP", Server, Port, Path ?? defaultNamingContext);
			entry.Username = BindUser;
			entry.Password = BindPass;
			
			
			DirectorySearcher ds = new DirectorySearcher (entry);
			ds.Filter = filter;
			ds.SearchScope = scope;
			ds.PageSize = 1000;
			
			if (Attributes != null) {
				ds.PropertiesToLoad.Clear ();
				foreach (string s in Attributes)
					ds.PropertiesToLoad.Add (s);
			}
			
			if (onlyone) {
				yield return ds.FindOne();
			} else {
			
				using (var src = ds.FindAll()) {
					foreach (SearchResult sr in src)
						yield return sr;
				}
			}
		}
		
		#region FindOne
		
		[System.Diagnostics.DebuggerStepThroughAttribute]
		public SearchResult FindOne (SearchScope scope, string Path, string filter, IEnumerable<string> Attributes)
		{
			return Search (true, scope, Path, filter, Attributes).FirstOrDefault ();
		}
		
		[System.Diagnostics.DebuggerStepThroughAttribute]
		public SearchResult FindOne (SearchScope scope, string Path, string filter)
		{
			return Search (true, scope, Path, filter, null).FirstOrDefault ();
		}
		
		[System.Diagnostics.DebuggerStepThroughAttribute]
		public SearchResult FindOne (string Path, string filter, IEnumerable<string> Attributes)
		{
			return Search (true, SearchScope.Subtree, Path, filter, Attributes).FirstOrDefault ();
		}
		
		[System.Diagnostics.DebuggerStepThroughAttribute]
		public SearchResult FindOne (string Path, string filter)
		{
			return Search (true, SearchScope.Subtree, Path, filter, null).FirstOrDefault ();
		}

		#endregion
		
		
		#region FindAll
		
		[System.Diagnostics.DebuggerStepThroughAttribute]
		public IEnumerable<SearchResult> FindAll (SearchScope scope, string Path, string filter, IEnumerable<string> Attributes)
		{
			return Search (false, scope, Path, filter, Attributes);
		}
		
		[System.Diagnostics.DebuggerStepThroughAttribute]
		public IEnumerable<SearchResult> FindAll (SearchScope scope, string Path, string filter)
		{
			return Search (false, scope, Path, filter, null);
		}
		
		[System.Diagnostics.DebuggerStepThroughAttribute]
		public IEnumerable<SearchResult> FindAll (string Path, string filter, IEnumerable<string> Attributes)
		{
			return Search (false, SearchScope.Subtree, Path, filter, Attributes);
		}
		
		[System.Diagnostics.DebuggerStepThroughAttribute]
		public IEnumerable<SearchResult> FindAll (string Path, string filter)
		{
			return Search (false, SearchScope.Subtree, Path, filter, null);
		}

		#endregion

	
		public SearchResult FetchUser (object hash)
		{
			string filter = "(&(objectClass=person)" + Map.Hash2Filter (hash) + ")";
			return FindOne (SearchScope.Subtree, null, filter, UserAttributes);
		}
		
		public SearchResult FetchUserById (string id)
		{
			string filter = "(&(objectClass=person)" + Map.Hash2Filter (new{userid=id}) + ")";
			return FindOne (SearchScope.Subtree, null, filter, UserAttributes);
		}
		
		public IEnumerable<SearchResult> FetchAllUsers (object hash)
		{
			string filter = "(&(objectClass=person)" + Map.Hash2Filter (hash) + ")";
			return FindAll (SearchScope.Subtree, null, filter, UserAttributes);
		}

		public IEnumerable<SearchResult> FetchAllUsers (string filter)
		{
			string Filter = "(&(objectClass=person)" + filter + ")";
			return FindAll (SearchScope.Subtree, null, Filter, UserAttributes);
		}
		
		private void PrepareFedora ()
		{
			
			#region Preparar roles y COS

			NewEntry ("cn=nsAccountInactivationTmp", (nentry) => {
				nentry.PropertyValues ("objectClass").AddRange (new string[] { "top", "nsContainer" });
				nentry.Properties ["cn"].Value = "cn=nsAccountInactivationTmp";
				nentry.CommitChanges ();
			});
			
			
			
			NewEntry (string.Format ("cn=\"cn={0},{1}\",cn=nsAccountInactivationTmp", "nsDisabledRole", defaultNamingContext), nentry => {
				nentry.PropertyValues ("objectClass").AddRange (new string[] { "top", "extensibleobject", "costemplate", "ldapsubentry" });
				nentry.Properties ["cosPriority"].Value = "1";
				nentry.Properties ["nsAccountLock"].Value = "true";
				nentry.CommitChanges ();
			});
			
			
			
			NewEntry ("cn=nsAccountInactivation_cos", nentry => {
				nentry.PropertyValues ("objectClass").AddRange (new string[] { 
	                "top", 
	                "LDAPSubentry",
	                "cosSuperDefinition",
	                "cosClassicDefinition"});
				
				nentry.Properties ["cosTemplateDn"].Value = "cn=nsAccountInactivationTmp," + defaultNamingContext;
				nentry.Properties ["cosSpecifier"].Value = "nsRole";
				nentry.Properties ["cosAttribute"].Value = "nsAccountLock operational";
	            
				nentry.CommitChanges ();
			});
				
				
			NewEntry ("cn=nsManagedDisabledRole", nentry => {
				nentry.PropertyValues ("objectClass").AddRange (new string[] {
	                "LDAPsubentry" ,
	                "nsRoleDefinition",
	                "nsSimpleRoleDefinition",
	                "nsManagedRoleDefinition"
	            });
				nentry.Properties ["cn"].Value = "cn=nsManagedDisabledRole";
				nentry.CommitChanges ();
			});
			
			
			
			NewEntry ("cn=nsDisabledRole", nentry => {
				
				nentry.PropertyValues ("objectClass").AddRange (new string[] {
	                "top" ,
	                "LDAPsubentry" ,
	                "nsRoleDefinition",
	                "nsComplexRoleDefinition",
	                "nsNestedRoleDefinition"
	            });

				nentry.Properties ["nsRoleDN"].Value = "cn=nsManagedDisabledRole";
				nentry.Properties ["cn"].Value = "cn=nsDisabledRole";
				nentry.CommitChanges ();
			
			});
			
			#endregion
			

			#region funciones
			IsDisabled = (x) => {
				
				foreach (KeyValuePair<string,string[]> attribute in x.ParseValues()) {
					if (String.Compare (attribute.Key, "nsroledn", StringComparison.CurrentCultureIgnoreCase) == 0) {
						foreach (string v in attribute.Value) {
							if (v.StartsWith ("cn=nsManagedDisabledRole"))
								return true;
						}
					}
				}
				
				return false;
			};
			
			
			Enable = (x) => {
				try {
					DirectoryEntry e = x.GetDirectoryEntry ();
					e.PropertyValues ("nsRoleDN").Remove ("cn=nsManagedDisabledRole," + defaultNamingContext);
					e.CommitChanges ();
					return true;
				} catch (Exception) {
					return false;
				}
			};
			
			
			Disable = (x) => {
				try {
					DirectoryEntry e = x.GetDirectoryEntry ();
					e.PropertyValues ("nsRoleDN").Add ("cn=nsManagedDisabledRole," + defaultNamingContext);
					e.CommitChanges ();
					return true;
				} catch (Exception) {
					return false;
				}
			};
			
			#endregion
		
			#region Propiedades
            
			Map.AddRange ("uid", "cn", "title", "telephoneNumber", "street", "memberOf", "postalCode", "sn", "givenname", "displayName", "mail", "objectClass", "description");
            
            
			foreach (var kvp in new{
				logon="uid",
				account="uid",
                samaccountname="uid",
                userid="uid",
                telephone="telephoneNumber",
                streetaddress="street",
                password="userPassword",
                @group="groupOfUniqueNames",
				name="givenname",
				surname="sn",
                
            }.ToStringHash()) {
				Map.Add (kvp.Key, String.Join (";", kvp.Value));
			}
            
            
			UserAttributes = Map.Translate ("objectClass", "userid", "nsroledn", "givenName", "sn", "telephoneNumber", "mail", "description", "creatorsName");
			UserMinimalAttributes = (from x in Map.Translate ("objectClass", "userid", "givenName", "sn") select x.ToLower()).ToArray();
			UserObjectClassValues = new string[]{"person", "organizationalPerson", "inetOrgPerson"};	
            
            #endregion
		}
		
		public Func<SearchResult, bool> IsDisabled = x => false;
		public Func<SearchResult, bool> Enable = x => false;
		public Func<SearchResult, bool> Disable = x => false;
		
		public DirectoryUser User { 
			get
			{
				return new DirectoryUser (this);
			}
		}
		
	}
}
